/*
 * Copyright (c) 2005, 2013, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

package com.sun.jmx.mbeanserver;


import static com.sun.jmx.mbeanserver.Util.*;

import java.lang.ref.WeakReference;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.List;
import java.util.WeakHashMap;

import javax.management.Descriptor;
import javax.management.ImmutableDescriptor;
import javax.management.IntrospectionException;
import javax.management.InvalidAttributeValueException;
import javax.management.MBeanAttributeInfo;
import javax.management.MBeanConstructorInfo;
import javax.management.MBeanException;
import javax.management.MBeanInfo;
import javax.management.MBeanNotificationInfo;
import javax.management.MBeanOperationInfo;
import javax.management.NotCompliantMBeanException;
import javax.management.NotificationBroadcaster;
import javax.management.ReflectionException;
import sun.reflect.misc.ReflectUtil;

/**
 * An introspector for MBeans of a certain type.  There is one instance
 * of this class for Standard MBeans, and one for every MXBeanMappingFactory;
 * these two cases correspond to the two concrete subclasses of this abstract
 * class.
 *
 * @param <M> the representation of methods for this kind of MBean: Method for Standard MBeans,
 * ConvertingMethod for MXBeans.
 * @since 1.6
 */
/*
 * Using a type parameter <M> allows us to deal with the fact that
 * Method and ConvertingMethod have no useful common ancestor, on
 * which we could call getName, getGenericReturnType, etc.  A simpler approach
 * would be to wrap every Method in an object that does have a common
 * ancestor with ConvertingMethod.  But that would mean an extra object
 * for every Method in every Standard MBean interface.
 */
abstract class MBeanIntrospector<M> {

  static final class PerInterfaceMap<M>
      extends WeakHashMap<Class<?>, WeakReference<PerInterface<M>>> {

  }

  /**
   * The map from interface to PerInterface for this type of MBean.
   */
  abstract PerInterfaceMap<M> getPerInterfaceMap();

  /**
   * The map from concrete implementation class and interface to
   * MBeanInfo for this type of MBean.
   */
  abstract MBeanInfoMap getMBeanInfoMap();

  /**
   * Make an interface analyzer for this type of MBean.
   */
  abstract MBeanAnalyzer<M> getAnalyzer(Class<?> mbeanInterface)
      throws NotCompliantMBeanException;

  /**
   * True if MBeans with this kind of introspector are MXBeans.
   */
  abstract boolean isMXBean();

  /**
   * Find the M corresponding to the given Method.
   */
  abstract M mFrom(Method m);

  /**
   * Get the name of this method.
   */
  abstract String getName(M m);

  /**
   * Get the return type of this method.  This is the return type
   * of a method in a Java interface, so for MXBeans it is the
   * declared Java type, not the mapped Open Type.
   */
  abstract Type getGenericReturnType(M m);

  /**
   * Get the parameter types of this method in the Java interface
   * it came from.
   */
  abstract Type[] getGenericParameterTypes(M m);

  /**
   * Get the signature of this method as a caller would have to supply
   * it in MBeanServer.invoke.  For MXBeans, the named types will be
   * the mapped Open Types for the parameters.
   */
  abstract String[] getSignature(M m);

  /**
   * Check that this method is valid.  For example, a method in an
   * MXBean interface is not valid if one of its parameters cannot be
   * mapped to an Open Type.
   */
  abstract void checkMethod(M m);

  /**
   * Invoke the method with the given target and arguments.
   *
   * @param cookie Additional information about the target.  For an MXBean, this is the MXBeanLookup
   * associated with the MXBean.
   */
    /*
     * It would be cleaner if the type of the cookie were a
     * type parameter to this class, but that would involve a lot of
     * messy type parameter propagation just to avoid a couple of casts.
     */
  abstract Object invokeM2(M m, Object target, Object[] args, Object cookie)
      throws InvocationTargetException, IllegalAccessException,
      MBeanException;

  /**
   * Test whether the given value is valid for the given parameter of this
   * M.
   */
  abstract boolean validParameter(M m, Object value, int paramNo,
      Object cookie);

  /**
   * Construct an MBeanAttributeInfo for the given attribute based on the
   * given getter and setter.  One but not both of the getter and setter
   * may be null.
   */
  abstract MBeanAttributeInfo getMBeanAttributeInfo(String attributeName,
      M getter, M setter);

  /**
   * Construct an MBeanOperationInfo for the given operation based on
   * the M it was derived from.
   */
  abstract MBeanOperationInfo getMBeanOperationInfo(String operationName,
      M operation);

  /**
   * Get a Descriptor containing fields that MBeans of this kind will
   * always have.  For example, MXBeans will always have "mxbean=true".
   */
  abstract Descriptor getBasicMBeanDescriptor();

  /**
   * Get a Descriptor containing additional fields beyond the ones
   * from getBasicMBeanDescriptor that MBeans whose concrete class
   * is resourceClass will always have.
   */
  abstract Descriptor getMBeanDescriptor(Class<?> resourceClass);

  /**
   * Get the methods to be analyzed to build the MBean interface.
   */
  final List<Method> getMethods(final Class<?> mbeanType) {
    ReflectUtil.checkPackageAccess(mbeanType);
    return Arrays.asList(mbeanType.getMethods());
  }

  final PerInterface<M> getPerInterface(Class<?> mbeanInterface)
      throws NotCompliantMBeanException {
    PerInterfaceMap<M> map = getPerInterfaceMap();
    synchronized (map) {
      WeakReference<PerInterface<M>> wr = map.get(mbeanInterface);
      PerInterface<M> pi = (wr == null) ? null : wr.get();
      if (pi == null) {
        try {
          MBeanAnalyzer<M> analyzer = getAnalyzer(mbeanInterface);
          MBeanInfo mbeanInfo =
              makeInterfaceMBeanInfo(mbeanInterface, analyzer);
          pi = new PerInterface<M>(mbeanInterface, this, analyzer,
              mbeanInfo);
          wr = new WeakReference<PerInterface<M>>(pi);
          map.put(mbeanInterface, wr);
        } catch (Exception x) {
          throw Introspector.throwException(mbeanInterface, x);
        }
      }
      return pi;
    }
  }

  /**
   * Make the MBeanInfo skeleton for the given MBean interface using
   * the given analyzer.  This will never be the MBeanInfo of any real
   * MBean (because the getClassName() must be a concrete class), but
   * its MBeanAttributeInfo[] and MBeanOperationInfo[] can be inserted
   * into such an MBeanInfo, and its Descriptor can be the basis for
   * the MBeanInfo's Descriptor.
   */
  private MBeanInfo makeInterfaceMBeanInfo(Class<?> mbeanInterface,
      MBeanAnalyzer<M> analyzer) {
    final MBeanInfoMaker maker = new MBeanInfoMaker();
    analyzer.visit(maker);
    final String description =
        "Information on the management interface of the MBean";
    return maker.makeMBeanInfo(mbeanInterface, description);
  }

  /**
   * True if the given getter and setter are consistent.
   */
  final boolean consistent(M getter, M setter) {
    return (getter == null || setter == null ||
        getGenericReturnType(getter).equals(getGenericParameterTypes(setter)[0]));
  }

  /**
   * Invoke the given M on the given target with the given args and cookie.
   * Wrap exceptions appropriately.
   */
  final Object invokeM(M m, Object target, Object[] args, Object cookie)
      throws MBeanException, ReflectionException {
    try {
      return invokeM2(m, target, args, cookie);
    } catch (InvocationTargetException e) {
      unwrapInvocationTargetException(e);
      throw new RuntimeException(e); // not reached
    } catch (IllegalAccessException e) {
      throw new ReflectionException(e, e.toString());
    }
        /* We do not catch and wrap RuntimeException or Error,
         * because we're in a DynamicMBean, so the logic for DynamicMBeans
         * will do the wrapping.
         */
  }

  /**
   * Invoke the given setter on the given target with the given argument
   * and cookie.  Wrap exceptions appropriately.
   */
    /* If the value is of the wrong type for the method we are about to
     * invoke, we are supposed to throw an InvalidAttributeValueException.
     * Rather than making the check always, we invoke the method, then
     * if it throws an exception we check the type to see if that was
     * what caused the exception.  The assumption is that an exception
     * from an invalid type will arise before any user method is ever
     * called (either in reflection or in OpenConverter).
     */
  final void invokeSetter(String name, M setter, Object target, Object arg,
      Object cookie)
      throws MBeanException, ReflectionException,
      InvalidAttributeValueException {
    try {
      invokeM2(setter, target, new Object[]{arg}, cookie);
    } catch (IllegalAccessException e) {
      throw new ReflectionException(e, e.toString());
    } catch (RuntimeException e) {
      maybeInvalidParameter(name, setter, arg, cookie);
      throw e;
    } catch (InvocationTargetException e) {
      maybeInvalidParameter(name, setter, arg, cookie);
      unwrapInvocationTargetException(e);
    }
  }

  private void maybeInvalidParameter(String name, M setter, Object arg,
      Object cookie)
      throws InvalidAttributeValueException {
    if (!validParameter(setter, arg, 0, cookie)) {
      final String msg =
          "Invalid value for attribute " + name + ": " + arg;
      throw new InvalidAttributeValueException(msg);
    }
  }

  static boolean isValidParameter(Method m, Object value, int paramNo) {
    Class<?> c = m.getParameterTypes()[paramNo];
    try {
      // Following is expensive but we only call this method to determine
      // if an exception is due to an incompatible parameter type.
      // Plain old c.isInstance doesn't work for primitive types.
      Object a = Array.newInstance(c, 1);
      Array.set(a, 0, value);
      return true;
    } catch (IllegalArgumentException e) {
      return false;
    }
  }

  private static void
  unwrapInvocationTargetException(InvocationTargetException e)
      throws MBeanException {
    Throwable t = e.getCause();
    if (t instanceof RuntimeException) {
      throw (RuntimeException) t;
    } else if (t instanceof Error) {
      throw (Error) t;
    } else {
      throw new MBeanException((Exception) t,
          (t == null ? null : t.toString()));
    }
  }

  /**
   * A visitor that constructs the per-interface MBeanInfo.
   */
  private class MBeanInfoMaker
      implements MBeanAnalyzer.MBeanVisitor<M> {

    public void visitAttribute(String attributeName,
        M getter,
        M setter) {
      MBeanAttributeInfo mbai =
          getMBeanAttributeInfo(attributeName, getter, setter);

      attrs.add(mbai);
    }

    public void visitOperation(String operationName,
        M operation) {
      MBeanOperationInfo mboi =
          getMBeanOperationInfo(operationName, operation);

      ops.add(mboi);
    }

    /**
     * Make an MBeanInfo based on the attributes and operations
     * found in the interface.
     */
    MBeanInfo makeMBeanInfo(Class<?> mbeanInterface,
        String description) {
      final MBeanAttributeInfo[] attrArray =
          attrs.toArray(new MBeanAttributeInfo[0]);
      final MBeanOperationInfo[] opArray =
          ops.toArray(new MBeanOperationInfo[0]);
      final String interfaceClassName =
          "interfaceClassName=" + mbeanInterface.getName();
      final Descriptor classNameDescriptor =
          new ImmutableDescriptor(interfaceClassName);
      final Descriptor mbeanDescriptor = getBasicMBeanDescriptor();
      final Descriptor annotatedDescriptor =
          Introspector.descriptorForElement(mbeanInterface);
      final Descriptor descriptor =
          DescriptorCache.getInstance().union(
              classNameDescriptor,
              mbeanDescriptor,
              annotatedDescriptor);

      return new MBeanInfo(mbeanInterface.getName(),
          description,
          attrArray,
          null,
          opArray,
          null,
          descriptor);
    }

    private final List<MBeanAttributeInfo> attrs = newList();
    private final List<MBeanOperationInfo> ops = newList();
  }

  /*
   * Looking up the MBeanInfo for a given base class (implementation class)
   * is complicated by the fact that we may use the same base class with
   * several different explicit MBean interfaces via the
   * javax.management.StandardMBean class.  It is further complicated
   * by the fact that we have to be careful not to retain a strong reference
   * to any Class object for fear we would prevent a ClassLoader from being
   * garbage-collected.  So we have a first lookup from the base class
   * to a map for each interface that base class might specify giving
   * the MBeanInfo constructed for that base class and interface.
   */
  static class MBeanInfoMap
      extends WeakHashMap<Class<?>, WeakHashMap<Class<?>, MBeanInfo>> {

  }

  /**
   * Return the MBeanInfo for the given resource, based on the given
   * per-interface data.
   */
  final MBeanInfo getMBeanInfo(Object resource, PerInterface<M> perInterface) {
    MBeanInfo mbi =
        getClassMBeanInfo(resource.getClass(), perInterface);
    MBeanNotificationInfo[] notifs = findNotifications(resource);
    if (notifs == null || notifs.length == 0) {
      return mbi;
    } else {
      return new MBeanInfo(mbi.getClassName(),
          mbi.getDescription(),
          mbi.getAttributes(),
          mbi.getConstructors(),
          mbi.getOperations(),
          notifs,
          mbi.getDescriptor());
    }
  }

  /**
   * Return the basic MBeanInfo for resources of the given class and
   * per-interface data.  This MBeanInfo might not be the final MBeanInfo
   * for instances of the class, because if the class is a
   * NotificationBroadcaster then each instance gets to decide what
   * MBeanNotificationInfo[] to put in its own MBeanInfo.
   */
  final MBeanInfo getClassMBeanInfo(Class<?> resourceClass,
      PerInterface<M> perInterface) {
    MBeanInfoMap map = getMBeanInfoMap();
    synchronized (map) {
      WeakHashMap<Class<?>, MBeanInfo> intfMap = map.get(resourceClass);
      if (intfMap == null) {
        intfMap = new WeakHashMap<Class<?>, MBeanInfo>();
        map.put(resourceClass, intfMap);
      }
      Class<?> intfClass = perInterface.getMBeanInterface();
      MBeanInfo mbi = intfMap.get(intfClass);
      if (mbi == null) {
        MBeanInfo imbi = perInterface.getMBeanInfo();
        Descriptor descriptor =
            ImmutableDescriptor.union(imbi.getDescriptor(),
                getMBeanDescriptor(resourceClass));
        mbi = new MBeanInfo(resourceClass.getName(),
            imbi.getDescription(),
            imbi.getAttributes(),
            findConstructors(resourceClass),
            imbi.getOperations(),
            (MBeanNotificationInfo[]) null,
            descriptor);
        intfMap.put(intfClass, mbi);
      }
      return mbi;
    }
  }

  static MBeanNotificationInfo[] findNotifications(Object moi) {
    if (!(moi instanceof NotificationBroadcaster)) {
      return null;
    }
    MBeanNotificationInfo[] mbn =
        ((NotificationBroadcaster) moi).getNotificationInfo();
    if (mbn == null) {
      return null;
    }
    MBeanNotificationInfo[] result =
        new MBeanNotificationInfo[mbn.length];
    for (int i = 0; i < mbn.length; i++) {
      MBeanNotificationInfo ni = mbn[i];
      if (ni.getClass() != MBeanNotificationInfo.class) {
        ni = (MBeanNotificationInfo) ni.clone();
      }
      result[i] = ni;
    }
    return result;
  }

  private static MBeanConstructorInfo[] findConstructors(Class<?> c) {
    Constructor<?>[] cons = c.getConstructors();
    MBeanConstructorInfo[] mbc = new MBeanConstructorInfo[cons.length];
    for (int i = 0; i < cons.length; i++) {
      final String descr = "Public constructor of the MBean";
      mbc[i] = new MBeanConstructorInfo(descr, cons[i]);
    }
    return mbc;
  }

}
